﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace EntityFramework.Helper.Searcher
{
    [System.ComponentModel.DataObject] 
    public abstract class Searcher<E> : ISearcher<E> where E : System.Data.Objects.DataClasses.EntityObject
    {
        private PropertyInfo m_contextEntityProperty;

        #region Consturctors

        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="context">The searcher's context.</param>
        protected Searcher(System.Data.Objects.ObjectContext context)
        {
            // get the current context type.
            Type contextType = context.GetType();

            // gets the current EntityType collection on the context.
            ContextEntityProperty = contextType.GetProperty(GetContextEntityPropertyName(context));
            Context = context;
        }

        #endregion

        #region Fields

        /// <summary>
        /// Gets or sets the name of the current entity type on the context.
        /// </summary>
        public PropertyInfo ContextEntityProperty
        {
            get;
            set;
        }

        private ICriteria<E> CurrentCriteria
        {
            get;
            set;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets the entity object property name on the context.
        /// </summary>
        /// <returns>The entity property name on the context.</returns>
        private static string GetContextEntityPropertyName(System.Data.Objects.ObjectContext context)
        {
            // Get the EntityObject type.
            Type entityObjectType = typeof(E);

            // Get all the context properties.
            PropertyInfo[] props = context.GetType().GetProperties();

            // creates an instance of Type to be used on the foreach.
            Type currentPropertyType;

            // execute a foreach to find out 
            // what's the name of the current entity type on the context.
            foreach (PropertyInfo prop in props)
            {
                // get the current property type.
                currentPropertyType = prop.PropertyType;

                // verifies if the current property type is generic,
                // which means it could be a collection of entity type.
                if (currentPropertyType.IsGenericType)
                {
                    // gets the generic arguments when it is derived from EntityObject
                    currentPropertyType = currentPropertyType.GetGenericArguments().
                                            Where(t => t.BaseType.Equals(typeof(System.Data.Objects.DataClasses.EntityObject))).FirstOrDefault();

                    // compares if the current property type is the same
                    // that the persister entity type.
                    if (currentPropertyType != null && currentPropertyType.Equals(entityObjectType))
                    {
                        // if it is equal, returns the property name.
                        return prop.Name;
                    }
                }
            }

            return String.Empty;
        }

        /// <summary>
        /// Verifies if the parameter objecy matches the current search criteria.
        /// </summary>
        /// <param name="obj">Object to be matched with the criteria.</param>
        /// <returns>True if the object matches, false if not.</returns>
        public bool MatchCriteria(E obj)
        {
            ICriteria<E> criteria = CurrentCriteria;
            PropertyInfo[] props = typeof(E).GetProperties().Where(p => criteria.ChangedProperties.ContainsKey(p.Name)).ToArray();

            foreach(PropertyInfo prop in props)
            {
                if (prop.GetValue(obj, new object[0]).Equals(criteria.ChangedProperties[prop.Name]))
                {
                    return true;
                }
            }

            return false;
        }

        #endregion

        #region ISearcher<E> Members

        /// <summary>
        /// Defines the context of the Persister.
        /// </summary>
        public System.Data.Objects.ObjectContext Context
        {
            get;
            set;
        }

        /// <summary>
        /// Returns all the object of E <see cref="System.Data.Objects.DataClasses.EntityObject"/> type on the context.
        /// </summary>
        /// <returns>All E <see cref="System.Data.Objects.DataClasses.EntityObject"/> objects on the context.</returns> 
        [System.ComponentModel.DataObjectMethod(System.ComponentModel.DataObjectMethodType.Select, true)]
        public E[] FindAll()
        {
            // return the entire collection of the EntityType from the context.
            return ((System.Data.Objects.ObjectSet<E>)ContextEntityProperty.GetValue(Context, new object[0])).ToArray();
        }

        /// <summary>
        /// Returns all the object of E <see cref="System.Data.Objects.DataClasses.EntityObject"/> type on the context ordered by the indicated field.
        /// </summary>
        /// <param name="orderBy">Order by field.</param>
        /// <returns>All E <see cref="System.Data.Objects.DataClasses.EntityObject"/> objects on the context, ordered by the field.</returns>
        [System.ComponentModel.DataObjectMethod(System.ComponentModel.DataObjectMethodType.Select, true)]
        public E[] FindAll(string orderBy = "asc")
        {
            // TODO: implement sort.

            // return the entire collection of the EntityType from the context.
            return ((System.Data.Objects.ObjectSet<E>)ContextEntityProperty.GetValue(Context, new object[0])).ToArray();
        }


        /// <summary>
        /// Returns all the objects that match the parameter criteria.
        /// </summary>
        /// <param name="criteria">The search criteria.</param>
        /// <returns>The objects that match the parameter criteria.</returns>
        [System.ComponentModel.DataObjectMethod(System.ComponentModel.DataObjectMethodType.Select, false)]
        public E[] FindAll(ICriteria<E> criteria)
        {
            CurrentCriteria = criteria;
            return ((System.Data.Objects.ObjectSet<E>)ContextEntityProperty.GetValue(Context, new object[0])).Where(MatchCriteria).ToArray();
        }

        /// <summary>
        /// Returns the first object of the context.
        /// </summary>
        /// <returns>The first object object of the context.</returns>
        public E FindFirst()
        {
            return ((System.Data.Objects.ObjectSet<E>)ContextEntityProperty.GetValue(Context, new object[0])).FirstOrDefault();
        }

        /// <summary>
        /// Returns the first object that matches the criteria.
        /// </summary>
        /// <param name="criteria">The search criteria.</param>
        /// <returns>The first object that matches the criteria.</returns>
        [System.ComponentModel.DataObjectMethod(System.ComponentModel.DataObjectMethodType.Select, false)]
        public E FindFirst(ICriteria<E> criteria)
        {
            CurrentCriteria = criteria;
            return ((System.Data.Objects.ObjectSet<E>)ContextEntityProperty.GetValue(Context, new object[0])).FirstOrDefault(MatchCriteria);
        }

        /// <summary>
        /// Returns only one object that matches the criteria.
        /// </summary>
        /// <param name="criteria">The search criteria.</param>
        /// <returns>Only one object that matches the criteria.</returns>
        /// <exception cref="">Throws this exceptiopn when the search criteria matches with more than one object.</exception>
        [System.ComponentModel.DataObjectMethod(System.ComponentModel.DataObjectMethodType.Select, false)]
        public E FindOne(ICriteria<E> criteria)
        {
            CurrentCriteria = criteria;
            var result = ((System.Data.Objects.ObjectSet<E>)ContextEntityProperty.GetValue(Context, new object[0])).Where(MatchCriteria);

            int resultCount = result.Count();

            if (resultCount > 0 && resultCount == 1)
            {
                return result.FirstOrDefault();
            }
            else
            {
                throw new InvalidOperationException("");
            }

        }

        #endregion

    }
}
